-module(user_store).


-define(DIM_DEFINE, [
                     {user,  [
                              {objectClass, ["inetOrgPerson", "organizationalPerson", "person", "top"]},
                              {primaryKey, uid},
                              {ou, "people"},
                              {attributes, [{uid, string}, {userPassword, string}, {cn, string}, {sn, string}, {mail, string}, {mobile, string}, {displayName, string}]},
                              {map, [{uid, login}, {displayName, name}]}
                             ]},

                     {organ, [
                              {objectClass, ["groupOfNames"]},
                              {primaryKey, ou},
                              {ou, "groups"},
                              {attributes, [{ou, string}, {cn, string}, {member, array}]},
                              {map, [{ou, id}, {cn, name}]}
                             ]},

                     {rating, [
                              {objectClass, ["groupOfNames"]},
                              {primaryKey, ou},
                              {ou, "ratings"},
                              {attributes, [{ou, string}, {cn, string}, {member, array}]},
                              {map, [{ou, id}, {cn, name}]}
                             ]},

                     {position, [
                              {objectClass, ["organizationalUnit"]},
                              {primaryKey, ou},
                              {ou, "positions"},
                              {attributes, [{ou, string}, {description, string}]},
                              {map, [{ou, id}, {description, name}]}
                             ]}
                   ]).

-export([
         init/0, init/1,
         add/2, destroy/2,
         find/2, find_all/1
        ]).

-export([
         add_user/2,
         update_user/2,
         find_all/0, find_all/2,
         find_one/1
        ]).
-export([
         userinfo/1, do/1
        ]).
-include_lib("eldap/include/eldap.hrl").


%% --------------------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------------------
init() ->
    F = fun(Handle) ->
                eldap:add(Handle, basedn(), [{"ou", ["tlm"]}, {"objectClass", ["organizationalUnit"]}]),
                {ok, basedn()}
        end,
    do(F),
    [ init(K) || {K, _} <- ?DIM_DEFINE ].


init(Dim) ->
    F = fun(Handle) ->
                eldap:add(Handle, basedn(Dim), [
                                                {"ou", [dim_define(Dim, ou)]}, 
                                                {"objectClass", ["organizationalUnit"]}
                                               ]),
                {ok, basedn(Dim)}
        end,
    do(F).


%% 列表
find_all(Dim) ->
    F = fun(Handle) ->
                Filters = lists:map(fun(X) -> eldap:equalityMatch("objectClass", X) end, dim_define(Dim, objectClass)),
                eldap:search(Handle, [{base, basedn(Dim)}, {filter, eldap:'and'(Filters)}, {scope, eldap:singleLevel()}])
        end,
    {ok, #eldap_search_result{entries = Entries}} = do(F),
    info(Dim, Entries).

%% 查找
find(Dim, Id) ->
    F = fun(Handle) ->
                Filters = lists:map(fun(X) -> eldap:equalityMatch("objectClass", X) end, dim_define(Dim, objectClass)),
                ValueFilter = eldap:equalityMatch(atom_to_list(pk(Dim)), Id),
                
                lager:info("find filter: ~p", [ValueFilter]),
                eldap:search(Handle, [{base, basedn(Dim)}, {filter, eldap:'and'([ValueFilter | Filters])}, {scope, eldap:singleLevel()}])
        end,
    {ok, #eldap_search_result{entries = Entries}} = do(F),
    info(Dim, Entries).




%% 增加
add(Dim, Attrs) ->
    AttrDefs = dim_define(Dim, attributes, []),
    ValFun = fun(K, T) -> 
                     Val = proplists:get_value(K, Attrs),
                     case T of
                         array -> Val;
                         _     -> [Val]
                     end
             end,
    Pk = atom_to_list(pk(Dim)),

    Id = case proplists:get_value(pk(Dim), Attrs) of
             undefined -> uuid:generate();
             <<"undefined">> -> uuid:generate();
             V         -> V
         end,
    Dn = dn(Dim, Id),

    Attrs1 = [
              {"objectClass", dim_define(Dim, objectClass)} |
              [ { atom_to_list(K), ValFun(K, V) } || {K, V} <- AttrDefs ]
             ],
    NewAttrs = [{Pk, [Id]} | proplists:delete(Pk, Attrs1)],

    F = case is_existed(Dim, Id) of
            true   -> 
                [OldAttrs] = find(Dim, Id),
                update_fn(Dim, Dn, NewAttrs, OldAttrs);
            false  -> 
                add_fn(Dn, NewAttrs)
        end,
    do(F),
    {ok, Id}.


%% 删除
destroy(Dim, Id) ->
    F = fun(Handle) ->
                eldap:delete(Handle, dn(Dim, Id)),
                {ok, Id}
        end,
    do(F).




% ================================================================================
% @doc 基础目录定义
% ================================================================================
basedn(Dim) ->
    "ou=" ++ dim_define(Dim, ou) ++ "," ++ basedn().
basedn() ->
    LdapConfig = application:get_env(cloap, ldap, []),
    proplists:get_value(basedn, LdapConfig).



% ================================================================================
% @doc 维度定义
% ================================================================================
dim_define(Dim, Key, DefVal) ->
    Options = proplists:get_value(Dim, ?DIM_DEFINE),
    proplists:get_value(Key, Options, DefVal).
dim_define(Dim, Key) ->
    dim_define(Dim, Key, undefined).


% ================================================================================
% @doc 获取 DN
% ================================================================================
dn(Dim, Id) when is_binary(Id) ->
    dn(Dim, binary_to_list(Id));
dn(Dim, Id) ->
    atom_to_list(pk(Dim)) ++ "=" ++ Id ++ "," ++ basedn(Dim).


% ================================================================================
% @doc 获取 PK 属性
% ================================================================================
pk(Dim) ->
    dim_define(Dim, primaryKey).
 

% ================================================================================
% @doc 获取 objectClass 属性
% ================================================================================
object_class(Dim) ->
    dim_define(Dim, objectClass).


% ================================================================================
% @doc 是否存在特定项
% ================================================================================
is_existed(Dim, Id) ->
    F = fun(Handle) ->
                ValueFilter = eldap:equalityMatch(atom_to_list(pk(Dim)), Id),
                ClassFilters = [ eldap:equalityMatch("objectClass", X) || X <- object_class(Dim) ],
                eldap:search(Handle, [{base, basedn(Dim)}, {filter, eldap:'and'([ValueFilter | ClassFilters])}, {scope, eldap:wholeSubtree()}])
        end, 
    {ok, #eldap_search_result{entries = Entries}} = do(F),
    length(Entries) > 0.




% ================================================================================
% @doc 数据转换为JSON格式
% ================================================================================
translate(Dim, Attrs, atom) ->
    translate(Dim, [ {atom_to_list(K), V} || {K, V} <- Attrs ]).
translate(Dim, Attrs) ->
    MapDef = [{atom_to_list(V), atom_to_list(K)} || {K, V} <- dim_define(Dim, map, [])],
    lists:map(
      fun({K1, V1}) ->
              K2 = proplists:get_value(K1, MapDef, K1),
              {K2, V1}
      end,
      Attrs).
reverse_translate(Dim, Attrs) ->
    MapDef = [{atom_to_list(K), atom_to_list(V)} || {K, V} <- dim_define(Dim, map, [])],
    AttrDef = dim_define(Dim, attributes, []),
    lists:map(
      fun({K1, V1}) ->
              K2 = proplists:get_value(K1, MapDef, K1),
              T  = proplists:get_value(list_to_atom(K1), AttrDef, string),
              {list_to_atom(K2), to_binary(V1, T)}
      end,
      Attrs).    

to_binary(Val, Type) when Type =:= array ->
    [ list_to_binary(X) || X <- Val ];
to_binary(Val, _Type) ->
    [ H | _ ] = Val,
    list_to_binary(H).

    


% ================================================================================
% @doc 数据转换为JSON格式
% ================================================================================
info(Dim, Entry) ->
    case is_list(Entry) of
        true -> lists:map(fun(X) -> info(Dim, X) end, Entry);
        false ->
            #eldap_entry{object_name = Dn, attributes = Attrs} = Entry,
            [ {dn, list_to_binary(Dn)} | reverse_translate(Dim, Attrs) ]
    end.


% ================================================================================
% @doc 获取数据属性值
% ================================================================================
attr_value({Key, Type}, Attrs) when is_atom(Key) ->
    attr_value({atom_to_list(Key), Type}, Attrs);
attr_value({Key, Type}, Attrs) ->
    Values = [ list_to_binary(X) || X <- proplists:get_value(Key, Attrs, []) ],
    case Type of
        array  -> Values;
        _Other ->
            case Values of
                [H|_] -> H;
                []    -> <<>>
            end
    end.


add_fn(Dn, Attr) ->
    fun(Handle) ->
            ok = eldap:add(Handle, Dn, Attr)
    end.

update_fn(Dim, Dn, Attr, OldAttr) ->

    OriginAttrs = translate(Dim, OldAttr, atom),
    lager:info("~p update: ~p, ~p replace ~p", [Dim, Dn, Attr, OriginAttrs]),


    fun(Handle) ->
            lists:foreach(
              fun({K, T}) ->
                      K1 = atom_to_list(K),
                      case T of
                          array ->
                              VLeft  = proplists:get_value(K1, Attr, []),
                              VRight = proplists:get_value(K1, OriginAttrs, []),

                              case erlang:length(VLeft -- VRight) of 
                                  L1 when L1 > 0 ->
                                      eldap:modify(Handle, Dn, [ eldap:mod_add(K1, (VLeft -- VRight)) ]);
                                  _ -> ok
                              end,
                              case erlang:length(VRight -- VLeft) of 
                                  L2 when L2 > 0 ->
                                      eldap:modify(Handle, Dn, [ eldap:mod_delete(K1, (VRight -- VLeft)) ]);
                                  _ -> ok
                              end;
                          _ ->
                              V = proplists:get_value(K1, Attr),
                              case V of
                                  undefined -> ok;
                                  _ -> 
                                      eldap:modify(Handle, Dn, [ eldap:mod_replace(K1, V) ])
                              end
                      end
              end,
              dim_define(Dim, attributes, [])
             )
    end.







userinfo(Entry) ->
    case is_list(Entry) of
        true -> lists:map(fun(X) -> userinfo(X) end, Entry);
        false ->
            #eldap_entry{object_name = Dn, attributes = Attrs} = Entry,
            #{
               dn     => list_to_binary(Dn),
               login  => attr_value({uid, string}, Attrs),
               name   => attr_value({displayName, string}, Attrs),
               mail   => attr_value({mail, string}, Attrs),
               mobile => attr_value({mobile, string}, Attrs)
             }
    end.





add_user(Uid, Attrs) ->
    F = fun(Handle) ->
                ok = eldap:add(Handle, uid(Uid), Attrs),
                {ok, uid(Uid)}
        end,
    do(F).




update_user(Uid, Attrs) ->
    F = fun(Handle) ->
                Ops = lists:map(
                        fun({K, V}) ->
                                eldap:mod_replace(K, V)
                        end, Attrs),
                ok = eldap:modify(Handle, uid(Uid), Ops),
                {ok, uid(Uid)} 
        end,
    do(F).


find_all(ObjClass, Group) ->
    F = fun(Handle) ->
                Filter = eldap:equalityMatch("objectClass", ObjClass),
                eldap:search(Handle, [{base, "ou=" ++ Group ++ "," ++ basedn()}, {filter, Filter}, {scope, eldap:singleLevel()}])
        end,
    {ok, #eldap_search_result{entries = Entries}} = do(F),
    Entries.
find_all() ->
    userinfo(find_all("inetOrgPerson", "people")).    


find_one(Uid) ->
    F = fun(Handle) ->
                Filter = eldap:equalityMatch("uid", Uid), 
                eldap:search(Handle, [{base, basedn()}, {filter, Filter}])
        end,
    
    case do(F) of
        {ok, #eldap_search_result{entries = [Entry]}} -> userinfo(Entry);
        _ -> not_found
    end.



do(Fun) ->
    LdapConfig = application:get_env(cloap, ldap, []),
    
    [Host, Port, BindDn, BindPasswd] =
        [ proplists:get_value(Item, LdapConfig) || Item <- [host, port, bind_dn, bind_password] ],
    
    try
        {ok, Handle} = eldap:open([Host], [{port, Port}]),
        ok = eldap:simple_bind(Handle, BindDn, BindPasswd),
        {ok, Result} = Fun(Handle),

        eldap:close(Handle),
        {ok, Result}
    catch
        _:Reason ->
            lager:error("ldap operation failed: ~p", [Reason]),
            {error, Reason}
    end.

uid(Uid) ->
    "uid=" ++ Uid ++ ",ou=people," ++ basedn().



